Skip to content

Add example for Debian without npm or Yarn#2454

Open
MikeMcC399 wants to merge 2 commits intonodejs:mainfrom
MikeMcC399:add-smaller-images-example-debian
Open

Add example for Debian without npm or Yarn#2454
MikeMcC399 wants to merge 2 commits intonodejs:mainfrom
MikeMcC399:add-smaller-images-example-debian

Conversation

@MikeMcC399
Copy link
Copy Markdown
Contributor

Description

A Debian example is added to Smaller images without npm/yarn to complement the existing Alpine example.

Motivation and Context

Issue #404 requests an image without npm nor Yarn. This is partially achieved by removing Yarn from all future Node.js Docker images based on the upcoming Node.js 26 release and other higher releases.

The npm package manager npm is part of the official Node.js distribution. If the role of Node.js Docker images is to package exactly what Node.js bundles in its releases, then npm must also be included.

Nevertheless, some users do not want to run their Docker image with a package manager to achieve a lower Docker image size or to harden their image against package manager vulnerabilities.

The Docker and Node.js Best Practices document, in the section Smaller images without npm/yarn, already provides an example for Alpine images, using a multi-stage build. The first stage builds the app with npm, then in a second run-time only Docker build stage, only the app and node directory are copied, without copying any npm or yarn package manager directories.

Testing Details

cd $(mktemp -d)
npm init -y
npm install @vercel/ncc
echo 'console.log("Hello world!")' > index.js

Add "build": "ncc build index.js -o dist" to package.json scripts

cat > Dockerfile2 <<'EOT'
FROM node:24-trixie-slim AS builder
WORKDIR /build-stage
COPY package*.json ./
RUN npm ci
# Copy the files you need
COPY . ./
RUN npm run build

FROM debian:trixie-slim
# Create app directory
WORKDIR /usr/src/app
# Add required binaries
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init \
    && rm -rf /var/lib/apt/lists/* \
    && groupadd --gid 1000 node \
    && useradd --uid 1000 --gid node --shell /bin/bash --create-home node \
    && chown node:node ./
COPY --from=builder /usr/local/bin/node /usr/local/bin/
COPY --from=builder /usr/local/bin/docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
USER node
# Update the following COPY lines based on your codebase
COPY --from=builder /build-stage/node_modules ./node_modules
COPY --from=builder /build-stage/dist ./dist
# Run with dumb-init to not start node with PID=1, since Node.js was not designed to run as PID 1
CMD ["dumb-init", "node", "dist/index.js"]
EOT

docker build -t test-small-image-trixie .
docker run --rm test-small-image-trixie # Outputs "Hello world!"
docker run --rm --entrypoint node test-small-image-trixie --version # Outputs v24.14.1
docker run --rm --entrypoint which test-small-image-trixie node # Outputs /usr/local/bin/node
docker run --rm --entrypoint which test-small-image-trixie npm # No output (npm not installed)
docker run --rm --entrypoint which test-small-image-trixie yarn # No output (Yarn not installed)

Example Output

Hello world!
v24.14.1
/usr/local/bin/node

Image sizes Debian

Image Size (local) % reduction
node:24-trixie-slim 328.25 MB 0%
test-small-image-trixie 312.03 MB 3%

Image sizes Alpine (from PR #2410) testing steps

Image Size (local) % reduction
node:24-alpine3.23 225.05 MB 0%
test-small-image 215.55 MB 2%

Types of changes

  • Documentation
  • Version change (Update, remove or add more Node.js versions)
  • Variant change (Update, remove or add more variants, or versions of variants)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Other (none of the above)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING.md document.
  • All new and existing tests passed.

@MikeMcC399 MikeMcC399 force-pushed the add-smaller-images-example-debian branch from f5fe189 to b9f7af4 Compare April 13, 2026 12:15
@MikeMcC399
Copy link
Copy Markdown
Contributor Author

Relative size-saving depends on the size of the custom app
@sxa
Copy link
Copy Markdown
Member

sxa commented Apr 23, 2026

I'll get round to a proper review later but out if curiousity what' the reason behind:

since Node.js was not designed to run as PID 1

Does that cause problems?

@sxa
Copy link
Copy Markdown
Member

sxa commented Apr 23, 2026

I'm a little surprised the savings are so small with this approach. Unlikely to be something that many people would go for (especially for v26 which isn't including yarn) but having the example is reasonable.

@MikeMcC399
Copy link
Copy Markdown
Contributor Author

I'll get round to a proper review later but out if curiousity what' the reason behind:

since Node.js was not designed to run as PID 1

Does that cause problems?

That is described in https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals

and https://packages.debian.org/trixie/dumb-init where it says:

Package: dumb-init (1.2.5-3 and others)

wrapper script which proxies signals to a child

dumb-init is a simple process supervisor and init system designed to run as PID 1 inside minimal container environments (such as Docker).

Lightweight containers have popularized the idea of running a single process or service without normal init systems like systemd or sysvinit. However, omitting an init system often leads to incorrect handling of processes and signals, and can result in problems such as containers which can't be gracefully stopped, or leaking containers which should have been destroyed.

dumb-init acts as PID 1 and immediately spawns your command as a child process, taking care to properly handle and forward signals as they are received.

@MikeMcC399
Copy link
Copy Markdown
Contributor Author

I'm a little surprised the savings are so small with this approach. Unlikely to be something that many people would go for (especially for v26 which isn't including yarn) but having the example is reasonable.

It wouldn't motivate me personally in terms of size savings, however other people may have other criteria. Reducing vulnerabilities would be a better motive, as npm is regularly involved in new vulnerability discoveries.

The motivation for adding this example is to provide the basis to close off the requests to remove npm and Yarn.

If this can be merged, then I'll write up the justification to propose closing off the related issue(s).

Copy link
Copy Markdown
Member

@sxa sxa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as a mirror of the Alpine one.
It may be worth putting in that reference link to the BestPractices document in the comments but I'll leave that to you whether to do so or not - it doesn't affect my approval

@MikeMcC399
Copy link
Copy Markdown
Contributor Author

It may be worth putting in that reference link to the BestPractices document in the comments

I think that will need to be handled separately, as the document refers to two different ways of achieving the same means:

I wasn't familiar with either of these before you asked, so it would need some more research before adding any reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants